Pointeur intelligent ✱ ********************** Nous vous avons conseillé d’utiliser la classe **vector** de la STL pour gérer les allocations mémoire dynamique (principe RC0). Ainsi, lorsque vous voulez faire une liste d'objets de type *A*, aucun soucis, la classe *vector* convient et suffit amplement. Alors pourquoi parler des pointeurs ? A cause de l'héritage ! En effet, si vous voulez faire une liste d'objets appartenant à une même hiérarchie, alors le C++ ne saura pas gérer ! Prenons un exemple : supposons que nous ayons une classe mère *A* et deux classes filles *B* et *C*. Pour créer un container pouvant stocker des objets de type *A*, *B* ou *C*, il va falloir choisir un type comme argument du *template vector*. On choisira donc naturellement la classe mère de la hiérarchie et on déclarera un *vector*. Malheureusement, en C++, cette syntaxe sous-entend que tous les objets dans ce container ne peuvent être QUE du type *A*. Impossible alors de stocker des objets de type *B* ou *C*. Pourquoi ? Rappelons que même si *B* et *C* sont des enfants de *A*, ils ne prennent pas forcément la même place en mémoire car ils peuvent stocker des informations supplémentaires. Or, un *vector*, qui est en fait comme un tableau de *A* : *A[]*, ne peut malheureusement stocker que des objets de même taille, d'où le problème. Pour sortir de l'impasse, on devra utiliser un *vector* de pointeurs vers des objets de type *A* et la mécanique interne du C++ va nous garantir qu'elle sera capable de déterminer le type exact : *A*, *B* ou *C*, de l'objet désigné par chaque pointeur. Bref, on ne peut pas enterrer l'utilisation des pointeurs dans le C++ moderne, sauf si vous n'utilisez pas l'héritage ! Cependant, dans un soucis de modernité, on va utiliser des *smart pointeurs* alliant la rapidité du C++ à la sécurité d'un garbage collector présent dans les autres langages comme Java ou C#. Les shared_ptr ============== Présentation ------------ Nous rappelons que : .. panels:: :column: col-lg-5 p-2 .. raw:: html

Les mots-clefs new et delete SONT INTERDITS

.. raw:: html

Les fonctions malloc et free SONT INTERDITES

Si les opérations new/delete ne sont pas autorisées, alors comment que faire ? Nous allons vous présenter une nouvelle approche appelée **pointeurs intelligents** (**smart pointers**). Nous nous concentrons plus particulièrement sur les **shared pointer** qui allie l'efficacité des pointeurs avec la sécurité d'un garbage collector. En effet, un *shared_ptr* est un pointeur doublé d'un compteur d'utilisation vers l'entité désignée. Lorsque ce compteur atteint 0, alors le *shared_ptr* déclenche la destruction de l'objet comme le ferait un garbage collector. Syntaxe ------- Pour instancier un objet et l'associer à un *shared pointer*, vous **devez** utiliser la syntaxe suivante : .. panels:: :column: col-lg-9 p-2 | shared_ptr p = **make_shared** (paramètres du constructeur); | ou | **auto** p = make_shared(paramètres du constructeur); Le mot clef *auto* permet de simplifier la syntaxe des instanciations. Pour accéder aux membres de l'objet associé au smart pointeur, il suffit d'utiliser l'opérateur **->** classique des pointeurs. Voici un exemple ci-dessous : .. code-block:: #include #include using namespace std; struct Point { int x, y; Point(int a, int b) : x(a), y(b) { } void Aff() { cout << "(" << x << "," << y << ")"; } }; int main() { shared_ptr p1 = make_shared(1, 2); p1->Aff(); auto p2 = make_shared(4, 5); p2->Aff(); } >> (1,2) >> (4,5) .. warning:: Nous vous demandons d'utiliser les *shared_ptr* car ils constituent le bagage minimum d'un programmeur C++ moderne. Est-ce que les shared_ptr résolvent tous les problèmes de libération de mémoire ? Malheureusement non. En effet, en cas de référence cyclique : un objet 1 pointant vers un objet 2 contenant lui-même un pointeur vers l'objet 1. Dans cette configuration, même si aucun pointeur ne référence ces deux objets, ils ne vont pas être détruits car les compteurs de références resteront bloqué à 1 à cause du cycle. Dans ce cas, il faudra utiliser un mécanisme supplémentaire pour sortir de l'impasse (hors programme). Nous vous conseillons, dans votre cas, d'éviter cette situation. Exemple 1 --------- Voici un exemple qui indique comment un *shared_ptr* se comporte lors des appels de fonction : .. code-block:: #include #include using namespace std; struct Point { int x, y; Point(int a, int b) : x(a), y(b) { cout << "Point créé\n"; } ~Point() { cout << "Point détruit\n"; } void Aff() { cout << "(" << x << "," << y << ")"< & p1){ // passage par référence 5 cout << "test_1 compteur : " << p1.use_count() << "\n"; } void test_2(shared_ptr p2) { // passage par copie 7 cout << "test_2 compteur : " << p2.use_count() << "\n"; } int main() { 1 auto p = make_shared(1, 2); // instanciation 2 p->Aff(); 3 cout << "main compteur : " << p.use_count() << "\n"; 4 test_1(p); 6 test_2(p); 8 cout << "main compteur : " << p.use_count() << "\n"; } 1 >> Point créé 2 >> (1,2) 3 >> main compteur : 1 5 >> test_1 compteur : 1 7 >> test_2 compteur : 2 8 >> main compteur : 1 9 >> Point détruit Description ligne à ligne : * 1 *auto p = make_shared(1, 2)* : crée un objet de type *Point* et un *shared_ptr p* pointant sur cet objet. * 2 *p->Aff();* : affiche les coordonnées *(x,y)* du point en question. * 3 *cout << "main compteur"* : un seul *shared_ptr* point vers l'objet Point, le compteur est à 1. * 4 *test_1(p)* : appel de la fonction *test_1*, le shared_ptr est passé par référence. * 5 *cout << "test_1 compteur"*, il n'y a pas eu de nouveau pointeur créé, le compteur est toujours égal à 1. * 6 *test_2(p)* : appel de la fonction *test_2*, le *shared_ptr* est passé par copie, un nouveau *shared_ptr p2* désgine alors le même objet. * 7 *cout << "test_2 compteur"*, les deux *shared_ptr* *p* et *p2* pointent vers le même objet, le compteur vaut donc 2. * 8 *cout << "main compteur"*, au retour dans la fonction *main*, le *shared_ptr p2* a été détruit, le compteur retombe à 1. * 9 *}* fin de la fonction *main()*, le *shared_ptr p* est détruit, le compteur de tombe à 0, et l'objet *Point* est détruit automatiquement. Exemple 2 --------- Voici un exemple qui indique comment un *shared_ptr* se comporte avec des *vector* : .. code-block:: #include #include #include using namespace std; struct Point { int x, y; Point(int a, int b) : x(a), y(b) { cout << "Point créé : "; Aff(); } ~Point() { cout << "Point détruit : "; Aff(); } void Aff() { cout << "(" << x << "," << y << ")"<> L; 1 L.push_back( make_shared(1,2) ); 1 L.push_back( make_shared(3,4) ); 2 L.push_back(L[0]); 3 cout << "compteur L[0] : " << L[0].use_count() << "\n"; 4 cout << "compteur L[1] : " << L[1].use_count() << "\n"; cout << "L[1] détruit" << endl; 5 L.erase(L.begin()+1); 7 cout << "Fin du programme" << endl; } >> Point créé : (1,2) >> Point créé : (3,4) >> compteur L[0] : 2 >> compteur L[1] : 1 >> L[1] détruit 6 >> Point détruit : (3,4) >> Fin du programme 8 >> Point détruit : (1,2) Commentaires : * 1 Deux objets *Point* sont créés et insérés dans un *vector L*. * 2 Le *shared_ptr* *L[0]* est copié pour créer un 3ème élément de *L*. * 3 Le compteur de *L[0]* vaut 2 car il y a deux *shared_ptr* pointant vers le même objet : *L[0]* et *L[2]*. * 4 Le compteur de *L[1]* vaut 1 car il y a unique *shared_ptr* pointant vers cet objet. * 5 La ligne : *L.erase(L.begin()+1)* efface le *shared_ptr L[1]*, le compteur tombe à 0. * 6 Par conséquent le *Point : (3,4)* est détruit dans la foulée : *Point détruit : (3,4)* * 7 La fonction *main* se termine, la liste *L* est détruite ainsi que les *shared_ptr* qu'elle contient. * 8 Le compteur de l'objet *Point(1,2)* tombe alors à 0 et cet objet est automatiquement détruit : *Point détruit : (1,2)*. Quizzz ====== .. quiz:: smartp :title: Smart pointers * :quiz:`{"type":"FB","answer":"make_shared(3,5)"}` Donnez la syntaxe pour créer un shared pointer sur un objet de type *T* construit avec les paramètres 3 et 5 (sans ;). * :quiz:`{"type":"TF","answer":"F"}` Depuis un shared pointer *p*, pour accéder au paramètre *a*, dois-je écrire *p.a* ? * :quiz:`{"type":"FB","answer":"1"}` Quelle est la valeur affichée par le code suivant : .. code-block:: void test(shared_ptr p) { p->Aff(); } int main() { shared_ptr p = make_shared(1,2); for (int i = 0 ; i < 3 ; i++) test(p); cout << p.use_count(); return 0; } * :quiz:`{"type":"TF","answer":"F"}` Lorsque deux objets forment une dépendance cyclique, lorsqu'ils ne seront plus utilisés par le programme, l'utilisation de shared pointer permet de libérer ces objets contrairement aux pointeurs classiques.